三行代码的简单函数,却写出了几十行 Typescript 类型推导 您所在的位置:网站首页 typescript 联合类型 判断 三行代码的简单函数,却写出了几十行 Typescript 类型推导

三行代码的简单函数,却写出了几十行 Typescript 类型推导

2023-05-30 04:35| 来源: 网络整理| 查看: 265

场景

最近在设计一些基础的项目框架设计上的 sdk api,比如埋点系统、权限系统之类的,要提供一些便捷的封装方法给上层使用。于是遇到了这么个场景。

有一个对象常量里,存了一些方法,例如:

const METHODS = { a: () => "a" as const, b: () => "b" as const, c: () => "c" as const }

然后想要封装这样一个 hook 例如 useMethod 给上层的 React 上下文使用:

type MethodKey = keyof typeof METHODS function useMethods(keys: MethodKey[]) { return keys.map(key => METHODS[key]) } // case const [a, b] = useMethods(['a', 'b']) // expect to "a" a();

一切都简简单单,属于是日常到不能再日常的代码,可是当我在 IDE 里挪上去一看,这不对劲呀:

image.png

我预期这里应该类型直接就是字符串 a 了,怎么会是个联合类型?

image.png

摸鱼吃瓜式排查

我上上下下看了一遍类型推导,发现 keys.map(key => METHODS[key]) 这一句里,key 直接被推导成了 "a" | "b" | "c"。

image.png

所以理所当然的结果也是推导成了 "a" | "b" | "c"。

emmm......这还有些麻烦,先单独写个类型方法来推导结果试试,递归传入的数组泛型,取出每一次的 key 对应的 method,再组合为数组。

type MethodValue = typeof METHODS[K] type GetMethodValue = T extends [] ? [] : T extends [infer F extends MethodKey, ...infer Rest extends MethodKey[]] ? [MethodValue, ...GetMethodValue] : never

测试一下:

image.png

再将类型回到方法 useMethod 上带入却发现完全不行:

image.png

如果强行断言 map 返回的结果,则直接会被推导为 never 类型

image.png

元组大法

其实不难从代码里看出,之所以无法推导原因有两点,第一点是在 Typescript 编译时这个阶段,是无法推导这个函数泛型传参的多种形态中的 key 是怎样排序的,其次是在 map 方法中,key 值一直被推导成 "a" | "b" | "c" 导致。

所以如果我用元组作为泛型限定值,倒是可以实现:

type GetMethodValue = T extends [] ? [] : T extends [infer F extends MethodKey, ...infer Rest extends MethodKey[]] ? [MethodValue, ...GetMethodValue] : never function useMethods(keys: T) { return keys.filter((key): key is MethodKey => !!key).map((key) => METHODS[key]) as GetMethodValue } const [a, b] = useMethods(['a', 'b']) const valueA = a()

image.png

理解到这,我就思考虽然类型不能自动推导出元组的组合排列方式,但是我却可以写一个方法来实现推导联合类型生成元组。

type Permutation = [T] extends [never] ? [] : U extends T ? [U, ...Permutation] : never; // expect to ['a', 'b'] | ['b', 'a'] type value = Permutation

这是我之前在写 TypeChallenge 时写过的方法,这就派上用场了。

直接将 MethodKey 这个联合类型解成元组之后限定泛型 T,最后确实也可以成功推导结果。

type Permutation = [T] extends [never] ? [] : U extends T ? [U?, ...Permutation] : never; type MethodKey = keyof typeof METHODS type MethodValue = typeof METHODS[K] type GetMethodValue = T extends [] ? [] : T extends [infer F extends MethodKey, ...infer Rest extends MethodKey[]] ? [MethodValue, ...GetMethodValue] : never const METHODS = { a: () => "a" as const, b: () => "b" as const, c: () => "c" as const } function useMethods(keys: T) { return keys.filter((key): key is MethodKey => !!key) .map((key) => METHODS[key]) as GetMethodValue } const [a, b] = useMethods(['a', 'b']) const valueA = a()

image.png

感叹

只是一个三行代码就实现的简单方法,但要做出准确的结果推导却需要这么复杂的类型声明去铺垫,虽然最后写出来很爽,但也感叹作为库开发者的一方真是非常不容易,这当中为了类型推导,还增加了冗余的代码,为了支持元组的可选值,不得不将变量打为可选,从而需要先 filter 后 map 才能保证结果不会出现空值的类型推导。

身为一个前端,在写 Ts 时时不时就要为几个简单结果的推导准确性花上小半天时间,有时候也觉得很不值得,不知道其他语言在类型上是否也有类似的烦恼,也希望 Typescript 团队能有更好的类型推断手段演进。

本文最后的解决方案不一定为最佳解决方案,不过作者也在社区和搜索网站上检索过答案,最后也没找到满意的解答,如果有大佬能提供更好的思路,晚辈感激不尽!

image.png

后续 - 2023.05.26

时隔一年左右,再回头看这篇文章,发现当初还是太年轻写过的 TS 太少,现在来看这个问题,把 case 拷到 TS Playground,咔咔咔五分钟解决了。

思路仍旧是前面一开始的思路,不过是将判断结果从函数泛型参数转变为函数泛型结果。

const METHODS = { a: () => "a" as const, b: () => "b" as const, c: () => "c" as const } type MethodKey = keyof typeof METHODS type MethodValues = K extends readonly [infer F, ...infer Rest] ? [ F extends MethodKey ? typeof METHODS[F] : void , ...MethodValues ] : [] function useMethods(keys: K) { return keys.map(key => METHODS[key as MethodKey]) as MethodValues } // case const [a, b, a1, b1, c] = useMethods(['a', 'b', 'a', 'b', 'c'] as const) // expect to "a" a(); 结尾

以上就是本篇文章分享的全部内容了。

这里是 Xekin(/zi:kin/)。喜欢的掘友们可以点赞关注点个收藏~

最近摸鱼时间比较多,写了一些奇奇怪怪有用但又不是特别有用的工具,不过还是非常有意思的,之后会一一写文章分享出来,感谢各位支持。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有